package com.googlecode.mojo.springbeandoc;

import java.awt.Dimension;
import java.awt.Image;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;

import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.shared.model.fileset.util.FileSetManager;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.FileSystemResource;

import edu.uci.ics.jung.algorithms.layout.CircleLayout;
import edu.uci.ics.jung.algorithms.layout.DAGLayout;
import edu.uci.ics.jung.algorithms.layout.FRLayout;
import edu.uci.ics.jung.algorithms.layout.FRLayout2;
import edu.uci.ics.jung.algorithms.layout.ISOMLayout;
import edu.uci.ics.jung.algorithms.layout.KKLayout;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.algorithms.layout.SpringLayout;
import edu.uci.ics.jung.algorithms.layout.SpringLayout2;
import edu.uci.ics.jung.algorithms.layout.StaticLayout;
import edu.uci.ics.jung.graph.DirectedGraph;
import edu.uci.ics.jung.graph.DirectedSparseMultigraph;
import edu.uci.ics.jung.io.GraphMLWriter;
import edu.uci.ics.jung.visualization.VisualizationImageServer;
import edu.uci.ics.jung.visualization.decorators.ToStringLabeller;
import edu.uci.ics.jung.visualization.renderers.Renderer.VertexLabel.Position;

/**
 * @goal springbeandoc
 */
public class SpringBeanDocMojo
        extends AbstractMojo
    {
    /**
     * Output directory.
     * 
     * @parameter expression="${project.build.directory}"
     */
    private File outputDirectory;

    /**
     * Post process flag.
     * 
     * @parameter expression="false"
     */
    private boolean postProcess;

    /**
     * Filesets.
     * 
     * @parameter
     */
    private FileSet[] fileSets;

    /**
     * Properties.
     * 
     * @parameter
     */
    private Property[] properties;

    /**
     * Layout type. Possible values: Circle, DAG, FR, FR2, ISOM, KK, Spring,
     * Spring2, Static.
     * 
     * @parameter expression="Circle"
     */
    private String layoutType;

    /**
     * Graph width.
     * 
     * @parameter expression="1200"
     */
    private int width;

    /**
     * Graph height.
     * 
     * @parameter expression="1200"
     */
    private int height;

    private DefaultListableBeanFactory beanFactory = null;

    private DirectedGraph<String, String> graph = null;

    /**
     * DOCUMENT ME!
     * 
     * @throws MojoExecutionException
     *             DOCUMENT ME!
     * @throws MojoFailureException
     *             DOCUMENT ME!
     */
    public void execute()
            throws MojoExecutionException, MojoFailureException
        {
        if (properties != null)
            {
            for (Property property : properties)
                {
                System.setProperty(property.getName(), property.getValue());
                }
            }

        beanFactory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader beanDefReader =
                new XmlBeanDefinitionReader(beanFactory);

        FileSetManager fileSetManager = new FileSetManager();

        if (fileSets != null)
            {
            for (FileSet fileSet : fileSets)
                {
                if (fileSet != null)
                    {
                    File dir = new File(fileSet.getDirectory());
                    String[] filenames =
                            fileSetManager.getIncludedFiles(fileSet);
                    for (String filename : filenames)
                        {
                        File file = new File(dir, filename);
                        try
                            {
                            beanDefReader
                                    .loadBeanDefinitions(new FileSystemResource(
                                            file));
                            }
                        catch (Exception e)
                            {
                            throw new MojoExecutionException(
                                    "Error reading bean definitions from "
                                            + file, e);
                            }
                        }
                    }
                }
            }

        if (postProcess)
            {
            Map<String, BeanFactoryPostProcessor> beanFactoryPostProcessorMap =
                    beanFactory.getBeansOfType(BeanFactoryPostProcessor.class,
                            true, false);
            for (BeanFactoryPostProcessor beanFactoryPostProcessor : beanFactoryPostProcessorMap
                    .values())
                {
                beanFactoryPostProcessor.postProcessBeanFactory(beanFactory);
                }
            }

        graph = new DirectedSparseMultigraph<String, String>();
        for (String name : beanFactory.getBeanDefinitionNames())
            {
            BeanDefinition beanDef = beanFactory.getBeanDefinition(name);
            processBeanDefinition(0, name, beanDef);
            }

        Layout<String, String> layout = null;
        if (layoutType.equals("Circle"))
            {
            layout = new CircleLayout<String, String>(graph);
            }
        else if (layoutType.equals("DAG"))
            {
            layout = new DAGLayout<String, String>(graph);
            }
        else if (layoutType.equals("FR"))
            {
            layout = new FRLayout<String, String>(graph);
            }
        else if (layoutType.equals("FR2"))
            {
            layout = new FRLayout2<String, String>(graph);
            }
        else if (layoutType.equals("ISOM"))
            {
            layout = new ISOMLayout<String, String>(graph);
            }
        else if (layoutType.equals("KK"))
            {
            layout = new KKLayout<String, String>(graph);
            }
        else if (layoutType.equals("Spring"))
            {
            layout = new SpringLayout<String, String>(graph);
            }
        else if (layoutType.equals("Spring2"))
            {
            layout = new SpringLayout2<String, String>(graph);
            }
        else if (layoutType.equals("Static"))
            {
            layout = new StaticLayout<String, String>(graph);
            }
        else
            {
            throw new MojoExecutionException("Unknown layout type: "
                    + layoutType);
            }

        Dimension d = new Dimension(width, height);
        VisualizationImageServer<String, String> visualization =
                new VisualizationImageServer<String, String>(layout, d);
        visualization.getRenderContext().setVertexLabelTransformer(
                new ToStringLabeller<String>());
        /*
         * visualization.getRenderContext().setEdgeLabelTransformer( new
         * ToStringLabeller<String>());
         */
        visualization.getRenderer().getVertexLabelRenderer().setPosition(
                Position.CNTR);
        Image image = visualization.getImage(visualization.getCenter(), d);
        try
            {
            File outputFile = new File(outputDirectory, "graph.png");
            ImageIO.write((RenderedImage) image, "PNG", outputFile);
            }
        catch (IOException e)
            {
            throw new MojoExecutionException("Error writing graph image", e);
            }

        GraphMLWriter<String, String> graphmlWriter =
                new GraphMLWriter<String, String>();
        Writer writer = null;
        try
            {
            File outputFile = new File(outputDirectory, "graph.xml");
            writer = new FileWriter(outputFile);
            graphmlWriter.save(graph, writer);
            }
        catch (IOException e)
            {
            throw new MojoExecutionException("Error writing graph XML", e);
            }
        finally
            {
            IOUtils.closeQuietly(writer);
            }
        }

    private void processValue(final int indent, final String parentName,
            final Object value)
        {
        if (value instanceof RuntimeBeanReference)
            {
            RuntimeBeanReference ref = (RuntimeBeanReference) value;
            String beanName = ref.getBeanName();
            if (beanFactory.isAlias(beanName))
                {
                String[] aliases = beanFactory.getAliases(beanName);
                beanName = aliases[0];
                }
            graph.addEdge(parentName + ":" + ref.hashCode(), parentName,
                    beanName);
            println(0, "bean-ref(%1$s)", beanName);
            }
        else if (value instanceof BeanDefinitionHolder)
            {
            BeanDefinitionHolder beanDefHolder = (BeanDefinitionHolder) value;
            graph.addEdge(parentName + ":" + beanDefHolder.getBeanName(),
                    parentName, beanDefHolder.getBeanName());
            processBeanDefinition(indent + 1, beanDefHolder.getBeanName(),
                    beanDefHolder.getBeanDefinition());
            System.out.println();
            }
        else if (value instanceof List)
            {
            List list = (List) value;
            println(0, "(list)");
            for (Object o : list)
                {
                print(indent, "- ");
                processValue(indent + 1, parentName, o);
                System.out.println();
                }
            }
        else if (value instanceof Map)
            {
            Map map = (Map) value;
            println(0, "(map)");
            for (Object key : map.keySet())
                {
                print(indent, "- ");
                processValue(indent + 1, parentName, key);
                System.out.print("=");
                processValue(indent + 1, parentName, map.get(key));
                System.out.println();
                }
            }
        else if (value instanceof TypedStringValue)
            {
            TypedStringValue v = (TypedStringValue) value;
            print(0, "%1$s", v.getValue());
            }
        else if (value instanceof String)
            {
            print(0, "%1$s", value);
            }
        else
            {
            print(0, "%1$s [%2$s]", value, value.getClass().getName());
            }
        }

    private void processValue(final int indent, final String name,
            final String subName, final Object value)
        {
        print(indent, "- %1$s=", subName);
        if (value instanceof RuntimeBeanReference)
            {
            RuntimeBeanReference ref = (RuntimeBeanReference) value;
            String beanName = ref.getBeanName();
            if (beanFactory.isAlias(beanName))
                {
                String[] aliases = beanFactory.getAliases(beanName);
                beanName = aliases[0];
                }
            println(0, "bean-ref(%1$s)", beanName);
            graph.addEdge(name + ":" + subName, name, beanName);
            }
        else
            {
            processValue(indent + 1, name, value);
            System.out.println();
            }
        }

    private void processBeanDefinition(final int indent, final String name,
            final BeanDefinition beanDef)
        {
        println(0, "bean-def(%1$s)", name);
        if (beanDef instanceof AbstractBeanDefinition)
            {
            AbstractBeanDefinition beanDef2 = (AbstractBeanDefinition) beanDef;
            int i = 0;
            if (beanDef2.getDependsOn() != null)
                {
                for (String dependsOn : beanDef2.getDependsOn())
                    {
                    String subName = "depends-on[" + i + "]";
                    println(indent, "- depends-on(%1$s)", subName);
                    graph.addEdge(subName, name, dependsOn);
                    i++;
                    }
                }
            }

        ConstructorArgumentValues constructorArgValues =
                beanDef.getConstructorArgumentValues();
        int i = 0;
        for (ConstructorArgumentValues.ValueHolder valueHolder : constructorArgValues
                .getGenericArgumentValues())
            {
            String subName = "constructor-arg[" + i + "]";
            Object value = valueHolder.getValue();
            processValue(indent + 1, name, subName, value);
            i++;
            }

        MutablePropertyValues propValues = beanDef.getPropertyValues();
        for (PropertyValue propValue : propValues.getPropertyValues())
            {
            String subName = propValue.getName();
            Object value = propValue.getValue();
            processValue(indent + 1, name, subName, value);
            }
        }

    private void print(final int indent, String format, Object... args)
        {
        for (int i = 0; i < indent; i++)
            {
            System.out.print(" ");
            }
        System.out.format(format, args);
        }

    private void println(final int indent, String format, Object... args)
        {
        print(indent, format, args);
        System.out.println();
        }
    }
